#  ethernet.rb
#
#  This file is part of myvnet.
#
#  Copyright (C) 2008  Filipe David Borba Manana,  fdmanana@gmail.com
#
#  myvnet is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  myvnet is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with myvnet.  If not, see <http://www.gnu.org/licenses/>.


require 'lib/protocols/generic_protocol'
require 'lib/protocols/ipv4'
require 'lib/protocols/arp'
require 'lib/exceptions'


# Ethernet II protocol module.
#
# *Notes*:
#
# * Ethernet II (DIX frames) only (EtherType field > 1500). 
# * No support for Ethernet 802.2 variants (requires SNAP and LLC protocols for determing payload type).
#
module Ethernet
  
  # maps ethertype codes to symbols
  ETHERTYPE_SYM = {
    0x08_00 => :ipv4,
    0x08_06 => :arp,
  }.freeze
  
  # maps ethertype symbols to codes
  ETHERTYPE_CODE = ETHERTYPE_SYM.invert.freeze
  
  # === Synopsis
  #
  #   Ethernet.to_friendly_addr(stream)
  #
  # === Args
  #
  # +stream+:: 
  #   A byte stream representing an Ethernet address (only first 6 bytes are processed,
  #   the rest of the stream is ignored).
  #
  # === Return
  #
  # String representing the Ethernet address as 6 hexadecimal values separated by 
  # collons (:) - the human friendly format.
  #
  # === Exceptions
  #
  # +BadArgType+:: 
  #   If stream parameter is not of type String (byte stream).
  # +BadArgFormat+:: 
  #   If length of the stream is less than 4 bytes.
  #
  # === Description
  #
  # Converts a byte stream, containing an Ethernet address, to a string representation in the
  # human friendly format (6 hexadecimal values separated by collons).
  # 
  def self.to_friendly_addr(stream)
    raise BadArgType, "Expected a byte stream" unless stream.is_a? String
    raise BadArgFormat, "Expected a byte stream with at least 6 bytes" unless stream.length >= 6
    
    bytes = stream.unpack 'C6'
    "%02x:%02x:%02x:%02x:%02x:%02x" % bytes
  end
  
  
  # === Synopsis
  #
  #   Ethernet.to_bstream_addr(address)
  #
  # === Args
  #
  # +address+:: 
  #   A string representing an Ethernet address in human friendly format
  #   (6 hexadecimal values separated by collons ":").
  #
  # === Return
  #
  # A byte stream, in network byte order, representation of the given address.
  #
  # === Exceptions
  #
  # +BadArgType+:: 
  #   If stream parameter is not of type String (byte stream).
  # +BadArgFormat+:: 
  #   If the given address is not a list of 6 hexadecimal values (2 digits each), 
  #   separated by collons.
  #
  # === Description
  #
  # Converts a string containing an Ethernet address in the human friendly format (6 hexadecimal
  # values separated by collons) to a byte stream in network byte order (big endian).
  #
  def self.to_bstream_addr(address)
    raise BadArgType, "Expected a String" unless address.is_a? String
    
    if !( address =~ /^\s* [0-9a-f]{2} : (?: [0-9a-f]{2} :){4} [0-9a-f]{2} \s*$/xi )
      raise BadArgFormat, "Expected an Ethernet address in human friendly format"
    end    
    
    bytes = (address.scan(/[0-9a-f]{2}/i)).map { |b| b.to_i(16) }        
    bytes.pack 'C6'
  end
  
  
  # Ethernet II packet.
  #
  class Packet < GenericProtocol::Packet
    
    HLEN = 14
    
    @packet_type = :ethernet
    @layer = 2
    
    attr_accessor :dst       # destination MAC address
    attr_accessor :src       # source MAC address
    attr_accessor :ethertype # payload type
    
    
    def initialize(bstream = nil)
      super
      
      if bstream.nil?
        bstream = ("\x00" * 12)
        bstream << "\x08\x00"
      end

      begin
        @dst = Ethernet.to_friendly_addr(bstream[0,6])
        @src = Ethernet.to_friendly_addr(bstream[6,6])
        @ethertype = (bstream[12,2].unpack 'n')[0]
        @ethertype = ETHERTYPE_SYM[@ethertype] || @ethertype
      rescue
        raise BadArgFormat, "Invalid byte stream for an Ethernet frame"
      end
      
      @payload = bstream[14..-1]      
    end
    

    def initialize_copy(orig)
      super
      @dst = orig.dst.dup
      @src = orig.src.dup
    end

        
    def build_upper_layers!
      return self unless payload? and @payload.is_a? String
      
      case @ethertype
        when :ipv4
          @payload = IPv4::Packet.new(@payload)
        when :arp
          @payload = ARP::Packet.new(@payload)
        else
          return self
      end
      
      @payload.build_upper_layers!
      @payload.parent = self
      
      self
    end
    
        
    def to_bstream
      stream = ''
      stream << Ethernet.to_bstream_addr(@dst)
      stream << Ethernet.to_bstream_addr(@src)
      
      if @ethertype.is_a? Symbol
        stream << ( [ ETHERTYPE_CODE[@ethertype] ].pack 'n' )        
      else
        stream << ( [@ethertype].pack 'n' )
      end
      
      case @payload
        when String
          stream << @payload
        when GenericProtocol::Packet
          stream << (@payload.to_bstream)
      end
      
      stream
    end
    

    # === Synopsis
    #
    #   Ethernet::Packet#has_tcp?
    #
    # === Return
    #
    # True if this packet encapsulates a TCP packet, false otherwise.
    #
    # === Description
    #
    # Verifies if this packet encapsulates a TCP packet.
    #
    def has_tcp?
      @ethertype == :ipv4 and @payload.proto == :tcp
    end
    

    # === Synopsis
    #
    #   Ethernet::Packet#has_udp?
    #
    # === Return
    #
    # True if this packet encapsulates a UDP packet, false otherwise.
    #
    # === Description
    #
    # Verifies if this packet encapsulates a UDP packet.
    #    
    def has_udp?
      @ethertype == :ipv4 and @payload.proto == :udp
    end
       

    # === Synopsis
    #
    #   Ethernet::Packet#has_icmp?
    #
    # === Return
    #
    # True if this packet encapsulates an ICMP packet, false otherwise.
    #
    # === Description
    #
    # Verifies if this packet encapsulates an ICMP packet.
    #    
    def has_icmp?
      @ethertype == :ipv4 and @payload.proto == :icmp
    end

    
    # === Synopsis
    #
    #   Ethernet::Packet#has_arp?
    #
    # === Return
    #
    # True if this packet encapsulates an ARP packet, false otherwise.
    #
    # === Description
    #
    # Verifies if this packet encapsulates an ARP packet.
    #    
    def has_arp?
      @ethertype == :arp
    end
    
    
    # === Synopsis
    #
    #   Ethernet::Packet#has_ipv4?
    #
    # === Return
    #
    # True if this packet encapsulates an IPv4 packet, false otherwise.
    #
    # === Description
    #
    # Verifies if this packet encapsulates an IPv4 packet.
    #    
    def has_ipv4?
      @ethertype == :ipv4
    end    
    
    
    def to_s(indent_level = 0)
      prefix = ' ' * indent_level
      str = ''
      str << prefix << "#{self.class.name}\n"
      str << prefix << "src: #{@src}, dst: #{@dst}, "
      
      str << "ethertype: "
      case @ethertype
        when Numeric
          str << ("%04X" % @ethertype)
        else
          str << "#{@ethertype.to_s}"        
      end
      
      str << ",\n" << prefix << "payload:\n"
      case @payload
        when String
          str << prefix << '   ' << hexdump(@payload) << "\n"
        when GenericProtocol::Packet
          str << @payload.to_s(indent_level + 3)
      end
      
      str
    end
    
    
  end
  
end
